using System;
using System.Data;
using System.Text;
using System.Collections;
using System.Globalization;

using BizElements.Core.ProviderComponents;

namespace BizElements.Core
{
	#region Documentation.
	/// <summary>
	/// Contains database table/view metadata and methods.
	/// Factory class for related IEntity, EntityCollection and ADO.NET objects.
	/// </summary>
	/// <remarks>Provides methods for the creation of the compatibile O/RM objects
	/// and contains metadata for the table/view it represents such as:
	/// <list type="bullet">
	///	<item>Catalog data to which to which the table belongs to.</item>
	///	<item>Table name and optional alias.</item>
	///	<item>Fields owned by the table.</item>
	///	<item>Primary key data.</item>
	///	<item>Foreign keys.</item>
	/// </list>
	/// <seealso cref="IDbColumn"/><seealso cref="DbRelation"/><seealso cref="RelationBucket"/>
	/// </remarks>	
	/// <example>
	/// The following example demonstrates how to use BizElements metadata classes to create ADO.NET DataColumn,
	/// DataTable, DataRelation, DataSet and Constraint objects.
	/// <code>
	/// EmployeesMeta employees = new EmployeesMeta();
	/// EmployeeTerritoriesMeta empTerritories = new EmployeeTerritoriesMeta();
	/// TerritoriesMeta territories = new TerritoriesMeta();
	/// RegionMeta regions = new RegionMeta();
	/// 
	/// // Create relations between tables.
	/// RelationBucket relations = new RelationBucket();
	/// relations.Add(employees, empTerritories, empTerritories.EmployeeID);
	/// relations.Add(territories, empTerritories, empTerritories.TerritoryID);
	/// relations.Add(regions, territories, territories.RegionID);
	/// 
	/// // Create a DataSet object with relations between DataTables and constraints 
	/// // (primary key, foreign key and unique constraints.
	/// DataSet ds = relations.NewDataSet("EmployeeData", true, true, true);
	/// </code>
	/// </example>
	#endregion
	[Serializable]
    //[Obsolete("Preffer using SealedDbTable class instead of DbTable. SealedDbTable class supports usage/design patterns which significantly optimize memory usage.")]
	public abstract class DbTable : IDbTable
	{
		#region Members.

		/// <summary>
		/// Table name.
		/// </summary>		
		string tableName;

		/// <summary>
		/// Object alias. By default it is equal to table name.
		/// </summary>
		string alias;

		/// <summary>
		/// Table fields.
		/// </summary>
		DbColumnCollection columns;

		/// <summary>
		/// Primary key fields.
		/// </summary>
		string[] primaryKeyFields;
	
		/// <summary>
		/// Specifies whether columns' aliases are prefixed and changed whenever the table alias is changed.
		/// </summary>
		bool columnAliasesArePrefixed;		

		#endregion
		
		#region Constructors.

		/// <summary>
		/// Initializes a new instance of the DbTable class.
		/// </summary>
		/// <param name="name">Table name.</param>
		/// <param name="alias">Object alias. If <b>null</b> then it will be equal to table name.</param>
		/// <param name="columns">Table fields.</param>
		/// <param name="primaryKey">Primary key fields.</param>
		/// <param name="setPrefixedColumnAliases">Specifies whether columns' aliases are prefixed with a table alias.</param>
		protected DbTable(string name, string alias, DbColumnCollection columns, string[] primaryKey, bool setPrefixedColumnAliases)
		{
			Init(name, alias, columns, primaryKey, setPrefixedColumnAliases);
		}

		/// <summary>
		/// Initializes object. Inherited class may set members via Init method.
		/// </summary>		
		/// <param name="name">Table name.</param>
		/// <param name="alias">Object alias. If <b>null</b> then it will be equal to the table name.</param>
		/// <param name="columns">Table fields.</param>
		/// <param name="primaryKey">Primary key fields.</param>
		/// <param name="setPrefixedColumnAliases">Specifies whether columns' aliases are prefixed with a table alias.</param>
		private void Init(string name, string alias, DbColumnCollection columns, string[] primaryKey, bool setPrefixedColumnAliases)
		{
			this.tableName = name;
			if (alias != null)
				this.alias = alias;
			else
				this.alias = name;						
				
			this.columns = columns;
			foreach (IDbColumn column in this.columns)
				column.Table = this;
			
			this.columnAliasesArePrefixed = setPrefixedColumnAliases;
			this.primaryKeyFields = primaryKey;
		}

		#endregion

		#region Properties.

		/// <summary>
		/// Gets catalog information to which the table/view belongs to.
		/// </summary>
		public abstract ICatalog Catalog{get;}

		/// <summary>
		/// Gets table name.
		/// </summary>
		public string TableName
		{
			get {return this.tableName;}
		}

		/// <summary>
		/// Gets object alias. By default it is equal to the table name.
		/// </summary>
		public string Alias
		{
			get {return this.alias;}
		}

		/// <summary>
		/// Gets table fields.
		/// </summary>
		public DbColumnCollection Columns
		{
			get {return this.columns;}
		}

		// Initialized when PrimaryKey getter is used for the first time. Caches computed PK fields collection.
		[NonSerialized]
		DbColumnCollection pkNonSerialized;

		/// <summary>
		/// Gets primary key fields.
		/// </summary>
		public DbColumnCollection PrimaryKey
		{
			get 
			{
				// Cache computed PK fields collection. 
				// Next time the property is accessed the PK will be returned faster.
				if (this.pkNonSerialized == null)
				{					
					this.pkNonSerialized = new DbColumnCollection();
					foreach (string pkFieldName in this.primaryKeyFields)
						this.pkNonSerialized.Add(this.columns.GetByColumnName(pkFieldName));
				}
					
				return this.pkNonSerialized;
			}
		}	

		/// <summary>
		/// Gets the value that specifies whether columns' aliases are prefixed and changed whenever the table alias is changed.
		/// </summary>
		/// <remarks>Usually columns aliases are prefixed with a table name/alias. This ensures the uniqueness 
		/// of column aliases when selecting two columns with the same name from two different tables.</remarks>
		public bool ColumnAliasesArePrefixed
		{
			get {return this.columnAliasesArePrefixed;}
		}

		/// <summary>
		/// Gets the relations used by foreign keys.
		/// Empty array if no foreign key relations have been defined.
		/// </summary>
		public DbRelation[] ForeignKeys
		{
			get {return (_ForeignKeys != null) ? _ForeignKeys : new DbRelation[0];}
		}

        /// <summary>Gets the collection of customized information associated with the table such.</summary>
        /// <value>A IPropertyCollection with custom information if overriden in derived classes. <b>null</b> if no custom information is configured.</value>
        /// <remarks>The ExtendedProperties property must be overriden in derived class to configure custom information for the table such as DBMS-specific properties.</remarks>
        public virtual IPropertyCollection<string> ExtendedProperties
        {
            get { return null; }
        }

        /// <summary>Returns <b>false</b> - not implemented.</summary>
        /// <value>Always <b>false</b></value>
        public bool IsView
        {
            get { return false; }
        }

		#endregion

		#region Factory. New methods.

		/// <summary>
		/// When implemented in inherited classes it should return a new empty entity compatibile with this IDbTable.
		/// </summary>
		/// <returns>New entity.</returns>
		public abstract IEntity NewEntity();

        /// <summary>
        /// When implemented in inherited classes it should return object that builds entities directly from retrieved data, ie. <see cref="IDataReader"/>.
        /// </summary>
        /// <returns>Entity filler.</returns>
        public abstract IObjectReader NewEntityReader();

		#endregion

		#region Methods.

		/// <summary>
		/// When implemented in inherited classes it should create another IDbTable object for the same table/view.
		/// </summary>
		/// <param name="cloneAlias">Clone alias. May be <b>null</b>.</param>
		/// <returns>Clone.</returns>
		public abstract IDbTable Clone(string cloneAlias);

        /// <summary>
        /// Creates another IDbTable object for the same table/view.
        /// </summary>
        /// <param name="cloneAlias">Clone alias.</param>
        /// <param name="setPrefixedAliases">Specifies whether cloned columns will have prefixed aliases.</param>
        /// <returns>Clone.</returns>
        public virtual IDbTable Clone(string cloneAlias, bool setPrefixedAliases)
        {
            // Eventually, this method should be abstract because optimal code is generated via generator.
            // Default implementation is provided for compatibility with code generated by previous versions of generator.
            DbTable clone = (DbTable)Clone(cloneAlias);
            if (setPrefixedAliases)
                clone.SetPrefixedAliases();

            return clone;
        }

        /// <summary>
        /// Changes aliases of all columns so that they start with prefix (usually table name).		
        /// Eg: Id -> MyTable_Id
        /// </summary>
        /// <remarks>Avoid using SetPrefixedAliases() as it changes internal state of table objects. 
        /// Use Clone(string, bool) method and constructor overloads to instantiate a new object, instead.
        /// The method will be marked as obsolete in a future release and eventually it may be removed alltogether.</remarks>
        private void SetPrefixedAliases()
        {
            this.columnAliasesArePrefixed = true;
        }

        /// <summary>Gets tables which reference the current table.</summary>
        /// <returns>Array of tables or empty array if the current table is not referenced by other objects.</returns>
        /// <remarks>The GetChildTables method must be implemented in derived class.</remarks>
        public virtual IDbTable[] GetChildTables()
        {
            throw new NotImplementedException(Messages.DbTable_GetChildTablesNotImplemented);
        }

        /// <summary>Gets relations where current table acts as a parent.</summary>
        /// <returns>Array of relations or empty array if the current table is not referenced by other objects.</returns>
        /// <remarks>The GetChildRelations method must be implemented in derived class.</remarks>
        public virtual DbRelation[] GetChildRelations()
        {
            throw new NotImplementedException(Messages.DbTable_GetChildRelationsNotImplemented);
        }

		#endregion

		#region Foreign keys - protected members.

		[NonSerialized]
		private DbRelation[] foreignKeys;

		/// <summary>
		/// Relations used by foreign keys.
		/// </summary>
		protected DbRelation[] _ForeignKeys
		{
			get
			{
				if (this.foreignKeys == null)
					InitForeignKeys();

				return this.foreignKeys;
			}
			set
			{
				this.foreignKeys = value;
			}
		}

		/// <summary>
		/// Initializes an array of relations used by foreign keys.
		/// Inherited classes must implement this method.
		/// The method is called when property <see cref="_ForeignKeys"/> is accessed for the first time.
		/// </summary>
		/// <remarks>It is not wise to call this method in a constructor. If a recursive relation is defined, 
		/// or there is a circular path between multiple tables, an endless loop of InitForeignKeys might occur. 
		/// It is best to call the method only when relations are required, i.e. accessed.</remarks>
		protected abstract void InitForeignKeys();

		#endregion

		#region Parent property names.

		private string[] parentEntityProperties = new string[0];
		
		/// <summary>
		/// Sets the names of the properties generated for the parent entities.
		/// </summary>
		/// <param name="propertyNames">Property names.</param>
		protected void SetParentEntityProperties(string[] propertyNames)
		{
			this.parentEntityProperties = (propertyNames != null) ? propertyNames : new string[0];
		}

		/// <summary>
		/// Gets the names of the properties generated for the parent entities.
		/// </summary>
		public string[] ParentEntityProperties
		{
			get { return this.parentEntityProperties; }
		}

		/// <summary>
		/// Gets the full property path for the specified parent entity field.
		/// </summary>
		/// <param name="parentIndex">Index of parent property name in the <see cref="ParentEntityProperties"/> array.</param>
		/// <param name="parentColumn">Parent entity field. If null only property name generated for parent entity is returned.</param>
		/// <returns>Parent entity property name followed by dot operator and parent field property name if <b>parentColumn</b> is defined; otherwise only parent entity property name.</returns>
		protected string GetParentProperty(int parentIndex, IDbColumn parentColumn)
		{
			string fullPropertyName = this.ParentEntityProperties[parentIndex];
			if (parentColumn != null)
				fullPropertyName += "." + parentColumn.PropertyName;

			return fullPropertyName;
		}

		/// <summary>
		/// Gets the full property path for the specified parent entity field.
		/// </summary>
		/// <param name="fk">Relation to the parent entity.</param>
		/// <param name="parentColumn">Parent entity field. If null only property name generated for parent entity is returned.</param>
		/// <returns>Parent entity property name followed by dot operator and parent field property name if <b>parentColumn</b> is defined; otherwise only parent entity property name.</returns>
		/// <remarks>Eg. the following code snippet returns "RegionParent.RegionDescription" property path.
		/// <code>
		/// public string GetRegionDescriptionPropertyPath()
		/// {
		///		TerritoriesMeta territories = new TerritoriesMeta();
		///		RegionMeta regions = new RegionMeta();
		///		return territories.GetParentProperty(territories.FK_RegionID, regions.RegionDescription);
		/// }
		/// </code>
		/// </remarks>
		public string GetParentProperty(DbRelation fk, IDbColumn parentColumn)
		{
			int idxParent = IndexOfRelation(fk);
			string fullPropertyName = (idxParent >= 0) ? GetParentProperty(idxParent, parentColumn) : null;
			return fullPropertyName;            
		}

		private int IndexOfRelation(DbRelation rel)
		{
			for (int idx = 0; idx < this.ForeignKeys.Length; idx++)
			{
				if (rel.HasEqualForeignKeyFieldsAs(this.ForeignKeys[idx]))
					return idx;
			}

			return -1;
		}

		#endregion
    }	
}